class Bulb():
def __init__(self, onOff=False): self.onOff = onOff
def turnOn(self): self.onOff = True
def turnOff(self): self.onOff = False
Python OOPs fundamentals
An introduction to Object Oriented programming using Python.
Increasingly it’s becoming important for Data professionals to become better at programming and modern programming is centered around Object Oriented programming paradigm. This article helps in explaining some important programming concepts which are mostly language agnostic but we will be using Python in this article.
Object-oriented programming (OOPs) is a programming paradigm that relies on the concept of classes and objects. The basic idea of OOP is to divide a sophisticated program into a number of objects that interact with each other to achieve the desired functionality. There are several advantages of using OOP for data science:
- Encapsulation: OOPs allow you to wrap data and the methods that operate on that data within a single unit (i.e., an object). This makes it easier to organize and manage your code, as well as protect the data from accidental modification.
- Code reusability: OOPs allow you to create reusable code by defining classes that can be used to create multiple objects with the same behavior. This makes it easier to develop and maintain your code, as you can reuse existing code instead of writing new code from scratch.
- Modularity: OOPs allow you to divide your code into smaller, modular units (i.e., objects). This makes it easier to understand and maintain your code, as you can focus on one piece of functionality at a time.
- Inheritance: OOPs allow you to create a new class that is a modified version of an existing class. This allows you to reuse code from the existing class and override or extend it as needed. This can save time and reduce the amount of code you need to write.
Overall, OOP can help data professionals organize and manage their code more effectively, making it easier to develop and maintain data science projects. Let’s dive into the OOPs concept.
1 What are Objects
and Classes
?
Classes
are the blueprint for defining an Object
. While an Object
is a collection of data/properties and their behaviors/methods.
For example- Think of a class Bulb
that will have a state (On/Off) and methods to turnOn and turnoff the bulb.
Now we can create multiple bulb objects from this Bulb
class.
= Bulb(onOff=True)
b1 = Bulb()
b2 print(f"Bulb 1 state is :{b1.onOff}, Bulb 2 state is :{b2.onOff}")
Bulb 1 state is :True, Bulb 2 state is :False
b1 and b2 are objects of the class Bulb
. Let’s use the turnOn and turnOff methods
to update the bulb properties
.
; b2.turnOn()
b1.turnOff()print(f"Bulb 1 state is :{b1.onOff}, Bulb 2 state is :{b2.onOff}")
Bulb 1 state is :False, Bulb 2 state is :True
We can see from the example above, a Bulb
object contains the onOff
property. Properties
are variables that contain information regarding the object of a class and Methods
like turnOn and turnOff in our Bulb
class are functions that have access to the properties
of a class. Methods
can accept additional parameters, modify properties and return values.
2 Class and Instance variables
In Python, properties can be defined in two ways -
- Class Variables - Class variables are shared by all objects of the class. A change in the class variable will change the value of that property in all the objects of the class.
- Instance Variables - Instance variables are unique to each instance or object of the class. A change in instance variable will change the value of the property in that specific object only.
class Employee:
# Creating a class variable
= "Microsoft"
companyName
def __init__(self, name):
# creating an instance variable
self.name = name
= Employee('Aayush')
e1 = Employee('John')
e2
print(f'Name :{e1.name}')
print(f'Company Name: {e1.companyName}')
print(f'Name :{e2.name}')
print(f'Company Name: {e2.companyName}')
Name :Aayush
Company Name: Microsoft
Name :John
Company Name: Microsoft
We can see above, the class variable is defined outside of the initializer and the instance variable is defined inside the initializer.
= "Amazon"
Employee.companyName print(e1.companyName, e2.companyName)
Amazon Amazon
We can see above changing a class variable in the Employee class changes the class variable in all objects of the class. Most of the time we will be using instance variables but knowledge about class variables can come in handy. Let’s look at an interesting use of class variable -
class Employee:
# Creating a class variable
= "Microsoft"
companyName = []
companyEmployees
def __init__(self, name):
# creating an instance variable
self.name = name
self.companyEmployees.append(self.name)
= Employee('Aayush')
e1 = Employee('John')
e2
print(f'Name :{e1.name}')
print(f'Team Members: {e1.companyEmployees}')
print(f'Name :{e2.name}')
print(f'Company Name: {e2.companyEmployees}')
Name :Aayush
Team Members: ['Aayush', 'John']
Name :John
Company Name: ['Aayush', 'John']
We can see above, we are saving all objects of the Employee
class in companyEmployees
which is a list shared by all objects of the class Employee
.
3 Class, Static and Instance methods
In Python classes, we have three types of methods -
- Class Methods - Class methods work with class variables and are accessible using the class name rather than its object.
- Static Methods - Static methods are methods that are usually limited to class only and not their objects. They don’t typically modify or access class and instance variables. They are used as utility functions inside the class and we don’t want the inherited class to modify them.
- Instance Methods - Instance methods are the most used methods and have access to instance variables within the class. They can also take new parameters to perform desired operations.
class Employee:
# Creating a class variable
= "Microsoft"
companyName = []
companyEmployees
def __init__(self, name):
# creating an instance variable
self.name = name
self.companyEmployees.append(self.name)
@classmethod
def getCompanyName(cls): # This is a class method
return cls.companyName
@staticmethod
def plusTwo(x): # This is a static method
return x+2
def getName(self): # This is an instance method
return self.name
= Employee('Aayush')
e1 print(f"Calling class method. Company name is {e1.getCompanyName()}")
print(f"Calling Static method. {e1.plusTwo(2)}")
print(f"Calling instance method. Employee name is {e1.getName()}")
Calling class method. Company name is Microsoft
Calling Static method. 4
Calling instance method. Employee name is Aayush
We can see above we use the @classmethod
decorator to define the class method. cls
is used to refer to the class just as self
is used to refer to the object of the class. The class method at least takes one argument cls
.
We use @staticmethod
decorator to define static class plusTwo
. We can see that static methods don’t take any argument like self
and cls
.
The most commonly used methods are instance methods and they can be defined without a decorator within the class. Just like the class method they take at least one argument which is self
by convention.
4 Access Modifiers
Access modifiers limit access to the variables and functions of a class. There are three types of access modifiers - public, protected, and private.
4.1 Public Attributes
Public attributes are those methods and properties which can be accessed anywhere inside and outside of the class. By default, all the member variables and functions are public.
class Employee:
def __init__(self, name):
self.name = name ## Public variable
def getName(self): ## Public method
return self.name
= Employee("Aayush")
e1 print(f"Employee Name: {e1.getName()}")
Employee Name: Aayush
In the case above, both property name
and method getName
are public attributes.
4.2 Protected Attributes
Protected attributes are similar to public attributes which can be accessed within the class and also available to subclasses. The only difference is the convention, which is to define each protected member with a single underscore “_”.
class Employee:
def __init__(self, name, project):
self.name = name ## Public variable
self._project = project ## Protected variable
def getName(self): ## Public method
return self.name
def _getProject(self): ## Protected method
return self._project
= Employee("Aayush", "Project Orland")
e1 print(f"Employee Name: {e1.getName()}")
print(f"Project Name: {e1._getProject()}")
Employee Name: Aayush
Project Name: Project Orland
In the case above, both property _project
and method _getProject
are protected attributes.
4.3 Private Attributes
Private attributes are accessible within the class but not outside of the class. To define a private attribute, prefix the method or property with the double underscore”_“.
class Employee:
def __init__(self, name, project, salary):
self.name = name ## Public variable
self._project = project ## Protected variable
self.__salary = salary
def getName(self): ## Public method
return self.name
def _getProject(self): ## Protected method
return self._project
def __getSalary(self): ## Protected method
return self.__salary
= Employee("Aayush", "Project Orland", "3500")
e1 print(f"Employee Name: {e1.getName()}")
print(f"Project Name: {e1.__getSalary()}")
Employee Name: Aayush
AttributeError: 'Employee' object has no attribute '__getSalary'
We can see above, __salary
property and __getSalary
method are both private attributes and when we call them outside of the class they throw an error that the 'Employee' object has no attribute '__getSalary'
.
5 Encapsulation
Encapsulation in OOP refers to binding data and the methods to manipulate that data together in a single unit, that is, class. Encapsulation is usually used to hide the state and representation of the object from the outside. A good use of encapsulation is to make all properties private of a class to prevent direct access from outside and use public methods to let the outside world communicate with the class.
class Employee:
def __init__(self, name, project, salary):
self.__name = name ## Public variable
self.__project = project ## Protected variable
self.__salary = salary
def getName(self): ## Public method
return self.__name
= Employee("Aayush", "Project Orland", "3500")
e1 print(f"Employee Name: {e1.getName()}")
Employee Name: Aayush
Encapsulation has several advantages -
- Properties of the class can be hidden from the outside world
- More control over what the outside world can access from the class
A good example of encapsulation would be an access control class based on username and password.
class Auth:
def __init__(self, userName=None, password=None):
self.__userName = userName
self.__password = password
def login(self, userName, password):
if (self.__userName == userName) and (self.__password == password):
print (f"Access granted to {userName}")
else:
print("Invalid credentials")
= Auth("Aayush", "whatever")
e1 "Aayush", "whatever") ## This will grant access
e1.login(
"Aayush", "aasdasd") ## This will say invalid creds
e1.login(## This will raise an error as private properties can't be accessed from outside. e1.__password
Access granted to Aayush
Invalid credentials
AttributeError: 'Auth' object has no attribute '__password'
As we can see above __username
and __password
are protected properties and can only be used by the class to grand or reject access requests.
6 Inheritance
Inheritance provides a way to create new classes from the existing classes. The new class will inherit all the non-private attributes(properties and methods) from the existing class. The new class can be called a child class and the existing class can be called a parent class.
import math
class Shape:
def __init__(self, name):
self.name = name
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
__init__(self, name = "Square")
Shape.self.edge = edge
## Overiding the getArea function
def getArea(self):
return self.edge**2
class Circle(Shape):
def __init__(self, radius):
## calling the constructor from parent class Shape
__init__(self, name = "Circle")
Shape.self.radius = radius
## Overiding the getArea function
def getArea(self):
return math.pi * (self.radius**2)
= Square(4)
obj1
obj1.printDetails()
= Circle(3)
obj2 obj2.printDetails()
This shape is called Square and area is 16.
This shape is called Circle and area is 28.274333882308138.
We can see above we defined a parent class Shape
and then we inherited it to create a Square
and Circle
child class. While defining the Square
and Circle
class we overwrote the getArea
function pertinent to the class but we used the printDetails
function from the parent class to print details about child classes. The more common example in the machine learning world would be to create your own models in Pytorch where we inherit from nn.Module
class to create a new model.
6.1 Use of super()
Function
super()
function comes into play when we implement inheritance. The super()
function is used to refer to the parent class without explicitly naming the class. super()
function can be used to access parent class properties, calling the parent class, and can be used as initializers. Let’s look at the example above and see how we can modify the Square
class to use super()
function.
class Shape:
= 100
maxArea def __init__(self, name): self.name = name
def getArea(self): pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
= 50
maxArea def __init__(self, edge):
super().__init__(name = "Square") ## Initializing parent class
self.edge = edge
def getName(self):
return super().maxArea
def getArea(self):
return self.edge**2
def printDetails(self):
super().printDetails() ## Calling a parent class function
print(f"Max area from Shape class: {super().maxArea}") ## Accessing parent class property
print(f"Max area from Square class: {self.maxArea}")
= Square(4)
obj1
obj1.getName() obj1.printDetails()
This shape is called Square and area is 16.
Max area from Shape class: 100
Max area from Square class: 50
As we can see in the example above we have used -
super().__init__
to initialize the parentShape
classsuper().printDetails()
function to use a method from parent classsuper().maxArea
to access a property of a parent class
There are many advantages of inheritance -
- Reusability - Inheritance makes the code reusable. Common methods and properties can be stored in a parent class and child classes can inherit these methods.
- Modification - Code modification becomes easier if we use inheritance, if we want to make a change in the base class function it will be propagated to the child classes.
- Extensibility - We can derive new classes from the old ones by keeping things we need in the derived class.
7 Polymorphism
Polymorphism refers to the same object exhibiting different forms and behaviors. For example consider our shape class which could be a square, rectangle, polygon, etc. Instead of writing multiple functions to get the area of these shapes, we can use a common function like getArea() and implement this function in the derived class.
import math
class Shape:
def __init__(self, name):
self.name = name
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
__init__(self, name = "Square")
Shape.self.edge = edge
## Overiding the getArea function
def getArea(self):
return self.edge**2
class Circle(Shape):
def __init__(self, radius):
## calling the constructor from parent class Shape
__init__(self, name = "Circle")
Shape.self.radius = radius
## Overiding the getArea function
def getArea(self):
return math.pi * (self.radius**2)
= Square(4)
obj1 print(f"Area of this {obj1.name} is {obj1.getArea()}")
= Circle(3)
obj2 print(f"Area of this {obj2.name} is {obj2.getArea()}")
Area of this Square is 16
Area of this Circle is 28.274333882308138
As we can see above there is a pre-defined dummy method called getArea
in the Shape
class. We override this method in the Square
and Circle
class. This technique is called method overriding
. The advantage of method overriding
is that the derived class can write its own specific implementation based on the requirement while using the same function name.
7.1 Abstract base classes
Abstract base classes define a set of methods and properties that a class must implement in order to inherit the parent class. This is a useful technique to enforce that certain functions within the derived class must exist. To define an abstract base class, we use the abc
module. The abstract base class inherits from the built-in ABC
class and we use the decorator @abstractmethod
to declare an abstract method.
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
__init__(self, name = "Square")
Shape.self.edge = edge
= Square(4)
obj1 print(f"Area of this {obj1.name} is {obj1.getArea()}")
TypeError: Can't instantiate abstract class Square with abstract methods getArea
We can see above that we have created a Shape
class from the ABC
class which has an abstract method getArea
. Since our child class Square
didn’t have getArea implemented we get an error instantiating this class.
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
__init__(self, name = "Square")
Shape.self.edge = edge
def getArea(self): return self.edge**2
= Square(4)
obj1 print(f"Area of this {obj1.name} is {obj1.getArea()}")
Area of this Square is 16
We can see above, once we implemented the getArea method, the code runs fine.
Abstract base classes serve as a blueprint for derived classes to implement methods that are required to run the function appropriately.
8 Conclusion
In this article, we learned about what is object-oriented programming and key concepts using Python. A good understanding of these concepts will lay a solid foundation for any software professional to write and understand python code better.
I hope you enjoyed reading it. If there is any feedback on the code or just the blog post, feel free to comment below or reach out on LinkedIn.